#!/usr/bin/python
# Copyright (c) 2017 Red Hat Inc.
# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt)
# SPDX-License-Identifier: GPL-3.0-or-later

from __future__ import annotations


DOCUMENTATION = r"""
module: manageiq_alerts

short_description: Configuration of alerts in ManageIQ
extends_documentation_fragment:
  - community.general.manageiq
  - community.general.attributes

author: Elad Alfassa (@elad661) <ealfassa@redhat.com>
description:
  - The manageiq_alerts module supports adding, updating and deleting alerts in ManageIQ.
attributes:
  check_mode:
    support: none
  diff_mode:
    support: none

options:
  state:
    type: str
    description:
      - V(absent) - alert should not exist,
      - V(present) - alert should exist.
    choices: ['absent', 'present']
    default: 'present'
  description:
    type: str
    description:
      - The unique alert description in ManageIQ.
      - Required when state is "absent" or "present".
  resource_type:
    type: str
    description:
      - The entity type for the alert in ManageIQ. Required when O(state=present).
    choices: ['Vm', 'ContainerNode', 'MiqServer', 'Host', 'Storage', 'EmsCluster', 'ExtManagementSystem', 'MiddlewareServer']
  expression_type:
    type: str
    description:
      - Expression type.
    default: hash
    choices: ["hash", "miq"]
  expression:
    type: dict
    description:
      - The alert expression for ManageIQ.
      - Can either be in the "Miq Expression" format or the "Hash Expression format".
      - Required if O(state=present).
  enabled:
    description:
      - Enable or disable the alert. Required if O(state=present).
    type: bool
  options:
    type: dict
    description:
      - Additional alert options, such as notification type and frequency.
"""

EXAMPLES = r"""
- name: Add an alert with a "hash expression" to ManageIQ
  community.general.manageiq_alerts:
    state: present
    description: Test Alert 01
    options:
      notifications:
        email:
          to: ["example@example.com"]
          from: "example@example.com"
    resource_type: ContainerNode
    expression:
      eval_method: hostd_log_threshold
      mode: internal
      options: {}
    enabled: true
    manageiq_connection:
      url: 'http://127.0.0.1:3000'
      username: 'admin'
      password: 'smartvm'
      validate_certs: false # only do this when you trust the network!

- name: Add an alert with a "miq expression" to ManageIQ
  community.general.manageiq_alerts:
    state: present
    description: Test Alert 02
    options:
      notifications:
        email:
          to: ["example@example.com"]
          from: "example@example.com"
    resource_type: Vm
    expression_type: miq
    expression:
      and:
        - CONTAINS:
            tag: Vm.managed-environment
            value: prod
        - not:
          CONTAINS:
            tag: Vm.host.managed-environment
            value: prod
    enabled: true
    manageiq_connection:
      url: 'http://127.0.0.1:3000'
      username: 'admin'
      password: 'smartvm'
      validate_certs: false # only do this when you trust the network!

- name: Delete an alert from ManageIQ
  community.general.manageiq_alerts:
    state: absent
    description: Test Alert 01
    manageiq_connection:
      url: 'http://127.0.0.1:3000'
      username: 'admin'
      password: 'smartvm'
      validate_certs: false # only do this when you trust the network!
"""

RETURN = r"""
"""

from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.manageiq import ManageIQ, manageiq_argument_spec


class ManageIQAlert:
    """Represent a ManageIQ alert. Can be initialized with both the format
    we receive from the server and the format we get from the user.
    """

    def __init__(self, alert):
        self.description = alert["description"]
        self.db = alert["db"]
        self.enabled = alert["enabled"]
        self.options = alert["options"]
        self.hash_expression = None
        self.miq_expressipn = None

        if "hash_expression" in alert:
            self.hash_expression = alert["hash_expression"]
        if "miq_expression" in alert:
            self.miq_expression = alert["miq_expression"]
            if "exp" in self.miq_expression:
                # miq_expression is a field that needs a special case, because
                # it is returned surrounded by a dict named exp even though we don't
                # send it with that dict.
                self.miq_expression = self.miq_expression["exp"]

    def __eq__(self, other):
        """Compare two ManageIQAlert objects"""
        return self.__dict__ == other.__dict__


class ManageIQAlerts:
    """Object to execute alert management operations in manageiq."""

    def __init__(self, manageiq):
        self.manageiq = manageiq

        self.module = self.manageiq.module
        self.api_url = self.manageiq.api_url
        self.client = self.manageiq.client
        self.alerts_url = f"{self.api_url}/alert_definitions"

    def get_alerts(self):
        """Get all alerts from ManageIQ"""
        try:
            response = self.client.get(f"{self.alerts_url}?expand=resources")
        except Exception as e:
            self.module.fail_json(msg=f"Failed to query alerts: {e}")
        return response.get("resources", [])

    def validate_hash_expression(self, expression):
        """Validate a 'hash expression' alert definition"""
        # hash expressions must have the following fields
        for key in ["options", "eval_method", "mode"]:
            if key not in expression:
                msg = f"Hash expression is missing required field {key}"
                self.module.fail_json(msg)

    def create_alert_dict(self, params):
        """Create a dict representing an alert"""
        if params["expression_type"] == "hash":
            # hash expression supports depends on https://github.com/ManageIQ/manageiq-api/pull/76
            self.validate_hash_expression(params["expression"])
            expression_type = "hash_expression"
        else:
            # actually miq_expression, but we call it "expression" for backwards-compatibility
            expression_type = "expression"

        # build the alret
        alert = dict(
            description=params["description"],
            db=params["resource_type"],
            options=params["options"],
            enabled=params["enabled"],
        )

        # add the actual expression.
        alert.update({expression_type: params["expression"]})

        return alert

    def add_alert(self, alert):
        """Add a new alert to ManageIQ"""
        try:
            result = self.client.post(self.alerts_url, action="create", resource=alert)

            msg = "Alert {description} created successfully: {details}"
            msg = msg.format(description=alert["description"], details=result)
            return dict(changed=True, msg=msg)
        except Exception as e:
            msg = "Creating alert {description} failed: {error}"
            if "Resource expression needs be specified" in str(e):
                # Running on an older version of ManageIQ and trying to create a hash expression
                msg = msg.format(
                    description=alert["description"], error="Your version of ManageIQ does not support hash_expression"
                )
            else:
                msg = msg.format(description=alert["description"], error=e)
            self.module.fail_json(msg=msg)

    def delete_alert(self, alert):
        """Delete an alert"""
        try:
            result = self.client.post(f"{self.alerts_url}/{alert['id']}", action="delete")
            msg = "Alert {description} deleted: {details}"
            msg = msg.format(description=alert["description"], details=result)
            return dict(changed=True, msg=msg)
        except Exception as e:
            msg = "Deleting alert {description} failed: {error}"
            msg = msg.format(description=alert["description"], error=e)
            self.module.fail_json(msg=msg)

    def update_alert(self, existing_alert, new_alert):
        """Update an existing alert with the values from `new_alert`"""
        new_alert_obj = ManageIQAlert(new_alert)
        if new_alert_obj == ManageIQAlert(existing_alert):
            # no change needed - alerts are identical
            return dict(changed=False, msg="No update needed")
        else:
            try:
                url = f"{self.alerts_url}/{existing_alert['id']}"
                result = self.client.post(url, action="edit", resource=new_alert)

                # make sure that the update was indeed successful by comparing
                # the result to the expected result.
                if new_alert_obj == ManageIQAlert(result):
                    # success!
                    msg = "Alert {description} updated successfully: {details}"
                    msg = msg.format(description=existing_alert["description"], details=result)

                    return dict(changed=True, msg=msg)
                else:
                    # unexpected result
                    msg = "Updating alert {description} failed, unexpected result {details}"
                    msg = msg.format(description=existing_alert["description"], details=result)

                    self.module.fail_json(msg=msg)

            except Exception as e:
                msg = "Updating alert {description} failed: {error}"
                if "Resource expression needs be specified" in str(e):
                    # Running on an older version of ManageIQ and trying to update a hash expression
                    msg = msg.format(
                        description=existing_alert["description"],
                        error="Your version of ManageIQ does not support hash_expression",
                    )
                else:
                    msg = msg.format(description=existing_alert["description"], error=e)
                self.module.fail_json(msg=msg)


def main():
    argument_spec = dict(
        description=dict(type="str"),
        resource_type=dict(
            type="str",
            choices=[
                "Vm",
                "ContainerNode",
                "MiqServer",
                "Host",
                "Storage",
                "EmsCluster",
                "ExtManagementSystem",
                "MiddlewareServer",
            ],
        ),
        expression_type=dict(type="str", default="hash", choices=["miq", "hash"]),
        expression=dict(type="dict"),
        options=dict(type="dict"),
        enabled=dict(type="bool"),
        state=dict(default="present", choices=["present", "absent"]),
    )
    # add the manageiq connection arguments to the arguments
    argument_spec.update(manageiq_argument_spec())

    module = AnsibleModule(
        argument_spec=argument_spec,
        required_if=[
            ("state", "present", ["description", "resource_type", "expression", "enabled", "options"]),
            ("state", "absent", ["description"]),
        ],
    )

    state = module.params["state"]
    description = module.params["description"]

    manageiq = ManageIQ(module)
    manageiq_alerts = ManageIQAlerts(manageiq)

    existing_alert = manageiq.find_collection_resource_by("alert_definitions", description=description)

    # we need to add or update the alert
    if state == "present":
        alert = manageiq_alerts.create_alert_dict(module.params)

        if not existing_alert:
            # an alert with this description doesn't exist yet, let's create it
            res_args = manageiq_alerts.add_alert(alert)
        else:
            # an alert with this description exists, we might need to update it
            res_args = manageiq_alerts.update_alert(existing_alert, alert)

    # this alert should not exist
    elif state == "absent":
        # if we have an alert with this description, delete it
        if existing_alert:
            res_args = manageiq_alerts.delete_alert(existing_alert)
        else:
            # it doesn't exist, and that's okay
            msg = "Alert '{description}' does not exist in ManageIQ"
            msg = msg.format(description=description)
            res_args = dict(changed=False, msg=msg)

    module.exit_json(**res_args)


if __name__ == "__main__":
    main()
